// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'), require('../css/css'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror', '../css/css'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode(
    'sass',
    function(config) {
      var cssMode = CodeMirror.mimeModes['text/css'];
      var propertyKeywords = cssMode.propertyKeywords || {},
        colorKeywords = cssMode.colorKeywords || {},
        valueKeywords = cssMode.valueKeywords || {},
        fontProperties = cssMode.fontProperties || {};

      function tokenRegexp(words) {
        return new RegExp('^' + words.join('|'));
      }

      var keywords = ['true', 'false', 'null', 'auto'];
      var keywordsRegexp = new RegExp('^' + keywords.join('|'));

      var operators = [
        '\\(',
        '\\)',
        '=',
        '>',
        '<',
        '==',
        '>=',
        '<=',
        '\\+',
        '-',
        '\\!=',
        '/',
        '\\*',
        '%',
        'and',
        'or',
        'not',
        ';',
        '\\{',
        '\\}',
        ':',
      ];
      var opRegexp = tokenRegexp(operators);

      var pseudoElementsRegexp = /^::?[a-zA-Z_][\w\-]*/;

      var word;

      function isEndLine(stream) {
        return !stream.peek() || stream.match(/\s+$/, false);
      }

      function urlTokens(stream, state) {
        var ch = stream.peek();

        if (ch === ')') {
          stream.next();
          state.tokenizer = tokenBase;
          return 'operator';
        } else if (ch === '(') {
          stream.next();
          stream.eatSpace();

          return 'operator';
        } else if (ch === "'" || ch === '"') {
          state.tokenizer = buildStringTokenizer(stream.next());
          return 'string';
        } else {
          state.tokenizer = buildStringTokenizer(')', false);
          return 'string';
        }
      }
      function comment(indentation, multiLine) {
        return function(stream, state) {
          if (stream.sol() && stream.indentation() <= indentation) {
            state.tokenizer = tokenBase;
            return tokenBase(stream, state);
          }

          if (multiLine && stream.skipTo('*/')) {
            stream.next();
            stream.next();
            state.tokenizer = tokenBase;
          } else {
            stream.skipToEnd();
          }

          return 'comment';
        };
      }

      function buildStringTokenizer(quote, greedy) {
        if (greedy == null) {
          greedy = true;
        }

        function stringTokenizer(stream, state) {
          var nextChar = stream.next();
          var peekChar = stream.peek();
          var previousChar = stream.string.charAt(stream.pos - 2);

          var endingString =
            (nextChar !== '\\' && peekChar === quote) ||
            (nextChar === quote && previousChar !== '\\');

          if (endingString) {
            if (nextChar !== quote && greedy) {
              stream.next();
            }
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            state.tokenizer = tokenBase;
            return 'string';
          } else if (nextChar === '#' && peekChar === '{') {
            state.tokenizer = buildInterpolationTokenizer(stringTokenizer);
            stream.next();
            return 'operator';
          } else {
            return 'string';
          }
        }

        return stringTokenizer;
      }

      function buildInterpolationTokenizer(currentTokenizer) {
        return function(stream, state) {
          if (stream.peek() === '}') {
            stream.next();
            state.tokenizer = currentTokenizer;
            return 'operator';
          } else {
            return tokenBase(stream, state);
          }
        };
      }

      function indent(state) {
        if (state.indentCount == 0) {
          state.indentCount++;
          var lastScopeOffset = state.scopes[0].offset;
          var currentOffset = lastScopeOffset + config.indentUnit;
          state.scopes.unshift({ offset: currentOffset });
        }
      }

      function dedent(state) {
        if (state.scopes.length == 1) return;

        state.scopes.shift();
      }

      function tokenBase(stream, state) {
        var ch = stream.peek();

        // Comment
        if (stream.match('/*')) {
          state.tokenizer = comment(stream.indentation(), true);
          return state.tokenizer(stream, state);
        }
        if (stream.match('//')) {
          state.tokenizer = comment(stream.indentation(), false);
          return state.tokenizer(stream, state);
        }

        // Interpolation
        if (stream.match('#{')) {
          state.tokenizer = buildInterpolationTokenizer(tokenBase);
          return 'operator';
        }

        // Strings
        if (ch === '"' || ch === "'") {
          stream.next();
          state.tokenizer = buildStringTokenizer(ch);
          return 'string';
        }

        if (!state.cursorHalf) {
          // state.cursorHalf === 0
          // first half i.e. before : for key-value pairs
          // including selectors

          if (ch === '-') {
            if (stream.match(/^-\w+-/)) {
              return 'meta';
            }
          }

          if (ch === '.') {
            stream.next();
            if (stream.match(/^[\w-]+/)) {
              indent(state);
              return 'qualifier';
            } else if (stream.peek() === '#') {
              indent(state);
              return 'tag';
            }
          }

          if (ch === '#') {
            stream.next();
            // ID selectors
            if (stream.match(/^[\w-]+/)) {
              indent(state);
              return 'builtin';
            }
            if (stream.peek() === '#') {
              indent(state);
              return 'tag';
            }
          }

          // Variables
          if (ch === '$') {
            stream.next();
            stream.eatWhile(/[\w-]/);
            return 'variable-2';
          }

          // Numbers
          if (stream.match(/^-?[0-9\.]+/)) return 'number';

          // Units
          if (stream.match(/^(px|em|in)\b/)) return 'unit';

          if (stream.match(keywordsRegexp)) return 'keyword';

          if (stream.match(/^url/) && stream.peek() === '(') {
            state.tokenizer = urlTokens;
            return 'atom';
          }

          if (ch === '=') {
            // Match shortcut mixin definition
            if (stream.match(/^=[\w-]+/)) {
              indent(state);
              return 'meta';
            }
          }

          if (ch === '+') {
            // Match shortcut mixin definition
            if (stream.match(/^\+[\w-]+/)) {
              return 'variable-3';
            }
          }

          if (ch === '@') {
            if (stream.match(/@extend/)) {
              if (!stream.match(/\s*[\w]/)) dedent(state);
            }
          }

          // Indent Directives
          if (
            stream.match(
              /^@(else if|if|media|else|for|each|while|mixin|function)/,
            )
          ) {
            indent(state);
            return 'def';
          }

          // Other Directives
          if (ch === '@') {
            stream.next();
            stream.eatWhile(/[\w-]/);
            return 'def';
          }

          if (stream.eatWhile(/[\w-]/)) {
            if (stream.match(/ *: *[\w-\+\$#!\("']/, false)) {
              word = stream.current().toLowerCase();
              var prop = state.prevProp + '-' + word;
              if (propertyKeywords.hasOwnProperty(prop)) {
                return 'property';
              } else if (propertyKeywords.hasOwnProperty(word)) {
                state.prevProp = word;
                return 'property';
              } else if (fontProperties.hasOwnProperty(word)) {
                return 'property';
              }
              return 'tag';
            } else if (stream.match(/ *:/, false)) {
              indent(state);
              state.cursorHalf = 1;
              state.prevProp = stream.current().toLowerCase();
              return 'property';
            } else if (stream.match(/ *,/, false)) {
              return 'tag';
            } else {
              indent(state);
              return 'tag';
            }
          }

          if (ch === ':') {
            if (stream.match(pseudoElementsRegexp)) {
              // could be a pseudo-element
              return 'variable-3';
            }
            stream.next();
            state.cursorHalf = 1;
            return 'operator';
          }
        } // cursorHalf===0 ends here
        else {
          if (ch === '#') {
            stream.next();
            // Hex numbers
            if (stream.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/)) {
              if (isEndLine(stream)) {
                state.cursorHalf = 0;
              }
              return 'number';
            }
          }

          // Numbers
          if (stream.match(/^-?[0-9\.]+/)) {
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            return 'number';
          }

          // Units
          if (stream.match(/^(px|em|in)\b/)) {
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            return 'unit';
          }

          if (stream.match(keywordsRegexp)) {
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            return 'keyword';
          }

          if (stream.match(/^url/) && stream.peek() === '(') {
            state.tokenizer = urlTokens;
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            return 'atom';
          }

          // Variables
          if (ch === '$') {
            stream.next();
            stream.eatWhile(/[\w-]/);
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            return 'variable-2';
          }

          // bang character for !important, !default, etc.
          if (ch === '!') {
            stream.next();
            state.cursorHalf = 0;
            return stream.match(/^[\w]+/) ? 'keyword' : 'operator';
          }

          if (stream.match(opRegexp)) {
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            return 'operator';
          }

          // attributes
          if (stream.eatWhile(/[\w-]/)) {
            if (isEndLine(stream)) {
              state.cursorHalf = 0;
            }
            word = stream.current().toLowerCase();
            if (valueKeywords.hasOwnProperty(word)) {
              return 'atom';
            } else if (colorKeywords.hasOwnProperty(word)) {
              return 'keyword';
            } else if (propertyKeywords.hasOwnProperty(word)) {
              state.prevProp = stream.current().toLowerCase();
              return 'property';
            } else {
              return 'tag';
            }
          }

          //stream.eatSpace();
          if (isEndLine(stream)) {
            state.cursorHalf = 0;
            return null;
          }
        } // else ends here

        if (stream.match(opRegexp)) return 'operator';

        // If we haven't returned by now, we move 1 character
        // and return an error
        stream.next();
        return null;
      }

      function tokenLexer(stream, state) {
        if (stream.sol()) state.indentCount = 0;
        var style = state.tokenizer(stream, state);
        var current = stream.current();

        if (current === '@return' || current === '}') {
          dedent(state);
        }

        if (style !== null) {
          var startOfToken = stream.pos - current.length;

          var withCurrentIndent =
            startOfToken + config.indentUnit * state.indentCount;

          var newScopes = [];

          for (var i = 0; i < state.scopes.length; i++) {
            var scope = state.scopes[i];

            if (scope.offset <= withCurrentIndent) newScopes.push(scope);
          }

          state.scopes = newScopes;
        }

        return style;
      }

      return {
        startState: function() {
          return {
            tokenizer: tokenBase,
            scopes: [{ offset: 0, type: 'sass' }],
            indentCount: 0,
            cursorHalf: 0, // cursor half tells us if cursor lies after (1)
            // or before (0) colon (well... more or less)
            definedVars: [],
            definedMixins: [],
          };
        },
        token: function(stream, state) {
          var style = tokenLexer(stream, state);

          state.lastToken = { style: style, content: stream.current() };

          return style;
        },

        indent: function(state) {
          return state.scopes[0].offset;
        },
      };
    },
    'css',
  );

  CodeMirror.defineMIME('text/x-sass', 'sass');
});
